# Copyright (c) 2019 Presto Labs Pte. Ltd.
# Author: xguo

import enum
import hashlib
import hmac
import logging
import threading
import time
import typing
import urllib.parse

import requests
import requests.auth

from coin.base.atomic import AtomicLong
from coin.base.timestamp import get_timestamp
from coin.exchange.base.kr_rest.auth_base import AuthKey
from coin.exchange.base.kr_rest.rest_client_base import RestUpdate
from coin.exchange.util.async_http_client import AsyncHttpClient
from coin.exchange.util.rate_limit import RateLimiter

BITSTAMP_API_HOST = 'www.bitstamp.net'
BITSTAMP_PRIV_API_HOST = 'priv-api.bitstamp.net'


class TransactionType(enum.IntEnum):
  DEPOSIT = 0
  WITHDRAWAL = 1
  MARKET_TRADE = 2
  SUB_ACCOUNT_TRANSFER = 14


class SortType(enum.Enum):
  ASCENDING = 'asc'
  DESCENDING = 'desc'


class BitstampOrderSide(enum.IntEnum):
  BUY = 0
  SELL = 1


class BitstampAuth(requests.auth.AuthBase):
  def __init__(self, key_file, rest_request_gap_sec=None):
    self._key = AuthKey.from_file(key_file)
    self._key.customer_id = self._key.get_value('customer_id')
    self._nonce = AtomicLong(int(time.time() * 1000))
    self._fix_message_portion = self._key.customer_id + self._key.access_key

    # Insert rest request gap to ensure correct nonce at the server side.
    self._request_lock = threading.Lock()
    self._prev_request_timestamp = None
    self._rest_request_gap = None
    if rest_request_gap_sec is not None and rest_request_gap_sec > 1e-6:
      self._rest_request_gap = int(rest_request_gap_sec * 1e9)

  def _busy_wait_if_necessary(self):
    if self._rest_request_gap is None:
      return

    if self._prev_request_timestamp is None:
      self._prev_request_timestamp = get_timestamp()
      return

    while True:
      with self._request_lock:
        elapsed_time = get_timestamp() - self._prev_request_timestamp
        if elapsed_time > self._rest_request_gap:
          self._prev_request_timestamp = get_timestamp()
          break

  def gen_auth(self):
    nonce = self._nonce.get_and_add(1)
    message = str(nonce) + self._fix_message_portion
    signature = hmac.new(self._key.secret_key_bytes, message.encode(), hashlib.sha256)
    signature_str = signature.hexdigest().upper()
    auth_info = {
        'key': self._key.access_key,
        'signature': signature_str,
        'nonce': nonce,
    }
    return auth_info

  def __call__(self, req: requests.PreparedRequest):
    self._busy_wait_if_necessary()
    auth_params = self.gen_auth()
    if req.method == 'POST':
      if req.body:
        if isinstance(req.body, str):
          params = urllib.parse.parse_qs(req.body)
        elif isinstance(req.body, bytes):
          params = urllib.parse.parse_qs(req.body.decode())
        else:
          raise TypeError('Unknown Type: %s', type(req.body))
        params.update(auth_params)
      else:
        params = auth_params
      req.prepare_body(data=params, files=None)
    else:
      raise ValueError('Unsupported method! %s' % req.method)
    return req


class BitstampSyncQueryer(object):
  def __init__(
      self,
      key_file,
      *,
      api_host=None,
      disable_ssl=False,
      rest_request_gap_sec=None,
      timeout=10,
  ):
    self._timeout = timeout
    self._auth = BitstampAuth(key_file, rest_request_gap_sec)
    scheme = 'http://' if disable_ssl else 'https://'
    self._api_host = scheme + (api_host or BITSTAMP_API_HOST)
    self.rate_limit = RateLimiter(100, 10)
    self._logger = logging.getLogger(__name__)

  def auth_request(self, method, path, params):
    assert method == 'POST', method
    url = urllib.parse.urljoin(self._api_host, path)
    response = None
    try:
      response = requests.request(method, url, data=params, auth=self._auth, timeout=self._timeout)
      content = response.json()
      return content
    except requests.exceptions.RequestException:
      self._logger.exception('RequestException: %s')
      raise
    except ValueError:
      self._logger.exception('Json decode: %s', response.content)
      raise
    except Exception as e:
      self._logger.exception('Unknown exception, %s', e)
      raise

  def _query_nocheck(self, method, path, params):
    req_timestamp = time.time()
    content = self.auth_request(method, path, params)
    res_timestamp = time.time()
    status = requests.codes.ok
    update = RestUpdate(req_timestamp, res_timestamp, content, status)
    return update

  def query(self, method, path, params=None, disable_rate_limit=False):
    if not disable_rate_limit:
      while not self.rate_limit.check():
        time.sleep(0.1)
    else:
      self.rate_limit.check()
    return self._query_nocheck(method, path, params)


class BitstampAsyncQueryer(object):
  def __init__(
      self,
      key_file,
      *,
      api_host=None,
      disable_ssl=False,
      timeout=10,
      max_connections=4,
      rest_request_gap_sec=None,
  ):
    self._timeout = timeout
    self._http_client = AsyncHttpClient(max_connections=max_connections)
    self._auth = BitstampAuth(key_file, rest_request_gap_sec)
    scheme = 'http://' if disable_ssl else 'https://'
    self._api_host = scheme + (api_host or BITSTAMP_API_HOST)

  def query(self, method, path, params=None):
    assert method == 'POST', method
    url = urllib.parse.urljoin(self._api_host, path)
    future = self._http_client.fetch(method=method,
                                     url=url,
                                     data=params,
                                     auth=self._auth,
                                     timeout=self._timeout)
    return future


class BitstampNativePrivateClient(object):
  def __init__(
      self,
      *,
      key_file,
      api_host=None,
      disable_ssl=False,
      timeout=10,
      max_connections=8,
      use_async=False,
      rest_request_gap_sec=None,
  ):
    if use_async:
      self._auth_queryer = BitstampAsyncQueryer(key_file,
                                                api_host=api_host,
                                                disable_ssl=disable_ssl,
                                                timeout=timeout,
                                                max_connections=max_connections,
                                                rest_request_gap_sec=rest_request_gap_sec)
    else:
      self._auth_queryer = BitstampSyncQueryer(key_file,
                                               api_host=api_host,
                                               timeout=timeout,
                                               rest_request_gap_sec=rest_request_gap_sec)

  def _query(self, method, path, params=None):
    if params is not None:
      params = {key: value for key, value in params.items() if value is not None}
    params = params or None
    if path[-1] != '/':
      path += '/'
    return self._auth_queryer.query(method, path, params=params)

  def query_account_balance(self, currency_pair=None):
    method = 'POST'
    path = 'api/v2/balance/' + (currency_pair or '')
    update = self._query(method, path)
    return update

  def query_user_transactions(self,
                              *,
                              currency_pair=None,
                              offset=None,
                              limit=None,
                              sort: typing.Union[None, SortType] = None,
                              since_timestamp=None):
    method = 'POST'
    path = 'api/v2/user_transactions/' + (currency_pair or '')
    if sort is not None:
      sort = sort.value
    params = {
        'offset': offset,
        'limit': limit,
        'sort': sort,
        'since_timestamp': since_timestamp,
    }
    update = self._query(method, path, params)
    return update

  def query_open_orders(self, *, currency_pair=None):
    method = 'POST'
    path = 'api/v2/open_orders/' + (currency_pair or 'all')
    update = self._query(method, path)
    return update

  def cancel_order(self, order_id):
    method = 'POST'
    path = 'api/v2/cancel_order'
    params = {
        'id': order_id,
    }
    update = self._query(method, path, params)
    return update

  def cancel_all_orders(self):
    method = 'POST'
    path = 'api/v2/cancel_all_order'
    update = self._query(method, path)
    return update

  def buy_limit_order(
      self,
      *,
      currency_pair: str,
      amount,
      price,
      daily_order: bool = True,
      ioc_order: bool = False,
  ):
    method = 'POST'
    path = 'api/v2/buy/' + currency_pair
    params = {
        'amount': amount,
        'price': price,
        'daily_order': daily_order,
        'ioc_order': ioc_order,
    }
    update = self._query(method, path, params)
    return update

  def sell_limit_order(
      self,
      *,
      currency_pair: str,
      amount,
      price,
      daily_order: bool = True,
      ioc_order: bool = False,
  ):
    method = 'POST'
    path = 'api/v2/sell/' + currency_pair
    params = {
        'amount': amount,
        'price': price,
        'daily_order': daily_order,
        'ioc_order': ioc_order,
    }
    update = self._query(method, path, params)
    return update
